﻿using DevExpress.ExpressApp;
using DevExpress.ExpressApp.DC;
using DevExpress.Persistent.Base;
using DevExpress.Utils;
using DevExpress.Xpo;
using DevExpress.Xpo.Helpers;
using DevExpress.Xpo.Metadata;
using System.Collections;
using System.ComponentModel;

namespace EasyXaf.EntityDesigner.BusinessObjects;

public class DomainModelCloner
{
    private readonly Session _targetSession;
    private readonly ITypesInfo _typesInfo;
    private Dictionary<IXPSimpleObject, IXPSimpleObject> _sourceToCloneMap;
    private List<CloneObjectReferenceEntry> _referencesList;

    protected Dictionary<IXPSimpleObject, IXPSimpleObject> SourceToCloneMap => _sourceToCloneMap;

    protected List<CloneObjectReferenceEntry> ReferencesList => _referencesList;

    public DomainModelCloner(Session targetSession)
    {
        _targetSession = targetSession;
        _typesInfo = XafTypesInfo.Instance;
    }

    private void RegisterReferencedObject(IXPSimpleObject clonedObject, XPMemberInfo memberInfo, IXPSimpleObject referencedObject)
    {
        if (clonedObject != null && referencedObject != null)
        {
            _referencesList.Add(new CloneObjectReferenceEntry(clonedObject, memberInfo, referencedObject));
        }
    }

    private void UpdateReferencesToClonedObjects()
    {
        foreach (var references in _referencesList)
        {
            if (_sourceToCloneMap.TryGetValue(references.ReferencedObject, out var value) && references.ReferencedObject != value)
            {
                if (IsCollection(references.MemberInfo))
                {
                    var list = (IList)references.MemberInfo.GetValue(references.ClonedObject);
                    list.Remove(references.ReferencedObject);
                    list.Add(value);
                }
                else
                {
                    references.MemberInfo.SetValue(references.ClonedObject, value);
                }
            }
        }
    }

    protected virtual void EndRecurrentClone()
    {
        _sourceToCloneMap.Clear();
        _sourceToCloneMap = null;
        _referencesList.Clear();
        _referencesList = null;
    }

    protected virtual bool StartRecurrentClone()
    {
        if (_sourceToCloneMap == null)
        {
            _sourceToCloneMap = [];
            _referencesList = [];
            return true;
        }
        return false;
    }

    protected virtual void CheckValues(IXPSimpleObject sourceObject, Type targetType)
    {
        if (sourceObject == null)
        {
            throw new Exception("sourceObject reference is null");
        }
        if (targetType == null)
        {
            throw new Exception("targetType isn't defined");
        }
        if (!typeof(IXPSimpleObject).IsAssignableFrom(targetType))
        {
            throw new Exception("Can't clone non IXPSimpleObject objects");
        }
    }

    protected virtual IXPSimpleObject InternalCloneTo(IXPSimpleObject sourceObject, Type targetType)
    {
        IXPSimpleObject simpleObject = null;
        if (sourceObject != null)
        {
            var key = sourceObject.Session.GetKeyValue(sourceObject);
            var targetObject = (IXPSimpleObject)_targetSession.GetObjectByKey(targetType, key);
            if (targetObject != null)
            {
                simpleObject = targetObject;
            }
            else if (typeof(IXpoCloneable).IsAssignableFrom(sourceObject.GetType()))
            {
                simpleObject = ((IXpoCloneable)sourceObject).CloneTo(targetType);
                _sourceToCloneMap[sourceObject] = simpleObject;
            }
            else
            {
                simpleObject = DefaultCloneTo(sourceObject, targetType);
            }
        }
        return simpleObject;
    }

    protected virtual string FormatNotClonedMemberValues(string currentValue, IXPSimpleObject sourceObject, List<XPMemberInfo> notClonedMembersInfo)
    {
        var values = new List<string>();
        if (currentValue != null)
        {
            values.Add(currentValue);
        }
        foreach (var item in notClonedMembersInfo)
        {
            var sourceMemberValue = GetSourceMemberValue(item, sourceObject);
            values.Add($"{item.Name}: {(sourceMemberValue == null ? "N/A" : sourceMemberValue.ToString())}");
        }
        return string.Join("\r\n", [.. values]);
    }

    protected virtual XPMemberInfo GetNotClonedInfoMemberName(XPClassInfo targetType)
    {
        var typeInfo = _typesInfo.FindTypeInfo(targetType.ClassType);
        if (typeInfo != null)
        {
            var source = typeInfo.FindAttributes<NotClonedInfoAttribute>(recursive: true);
            if (source.Count() == 1)
            {
                var memberInfo = targetType.FindMember(source.First().MemberName);
                if (memberInfo != null && memberInfo.MemberType == typeof(string))
                {
                    return memberInfo;
                }
            }
        }
        return null;
    }

    protected virtual bool ContainsMember(XPClassInfo classInfo, XPMemberInfo memberInfo)
    {
        foreach (var member in classInfo.Members)
        {
            if (member.Name == memberInfo.Name && member.MemberType == memberInfo.MemberType && member.IsReadOnly == memberInfo.IsReadOnly)
            {
                return true;
            }
        }
        return false;
    }

    protected virtual void CheckMemberInObjects(XPMemberInfo memberInfo, IXPSimpleObject sourceObject, IXPSimpleObject targetObject)
    {
        if (sourceObject == null || targetObject == null)
        {
            throw new Exception("One or more arguments are null.");
        }
        if (!ContainsMember(sourceObject.ClassInfo, memberInfo))
        {
            throw new Exception($"Source object doesn't contain member: {memberInfo.Name}");
        }
        if (!IsMemberCloneable(memberInfo))
        {
            throw new Exception($"Member \"{memberInfo.Name}\" can't be cloned.");
        }
        if (!ContainsMember(targetObject.ClassInfo, memberInfo))
        {
            throw new Exception($"Target object doesn't contain member: {memberInfo.Name}");
        }
    }

    protected XPMemberInfo FindAliasMemberInfo(XPMemberInfo sourceMemberInfo, bool findFirstMember)
    {
        Guard.ArgumentNotNull(sourceMemberInfo, nameof(sourceMemberInfo));

        XPMemberInfo result = null;

        if (sourceMemberInfo.IsAliased)
        {
            var persistentAliasAttribute = (PersistentAliasAttribute)sourceMemberInfo.GetAttributeInfo(typeof(PersistentAliasAttribute));
            if (persistentAliasAttribute != null)
            {
                var aliasExpression = persistentAliasAttribute.AliasExpression;
                if (aliasExpression != null)
                {
                    aliasExpression = aliasExpression.TrimStart('[').TrimEnd(']');
                    var array = aliasExpression.Split('.');
                    var classInfo = sourceMemberInfo.Owner;
                    foreach (string memberName in array)
                    {
                        if (classInfo == null)
                        {
                            result = null;
                            break;
                        }

                        result = classInfo.FindMember(memberName);
                        if (findFirstMember || result == null)
                        {
                            break;
                        }

                        classInfo = result.ReferenceType;
                    }
                }
            }
        }
        return result;
    }

    public virtual List<XPMemberInfo> GetAllPublicMembers(XPClassInfo classInfo)
    {
        var memberInfos = new List<XPMemberInfo>();
        while (classInfo.ClassType.Assembly != typeof(XPObject).Assembly)
        {
            foreach (var ownMember in classInfo.OwnMembers)
            {
                if (ownMember.IsPublic)
                {
                    memberInfos.Add(ownMember);
                }
            }
            classInfo = classInfo.BaseClass;
        }
        return memberInfos;
    }

    public virtual bool IsMemberCloneable(XPMemberInfo memberInfo)
    {
        if (memberInfo.IsAssociation
            && memberInfo.GetAssociatedMember().MemberType.IsGenericType
            && memberInfo.GetAssociatedMember().MemberType.GetGenericTypeDefinition() == typeof(IPersistentInterfaceData<>))
        {
            return false;
        }
        return IsMemberCloneableCore(memberInfo);
    }

    private bool IsMemberCloneableCore(XPMemberInfo memberInfo)
    {
        if (memberInfo == null)
        {
            return false;
        }

        if (HasNonCloneableAttribute(memberInfo))
        {
            return false;
        }

        var flag = memberInfo.IsPublic && !memberInfo.IsKey && (!memberInfo.IsReadOnly || memberInfo.IsAssociationList || (memberInfo.IsNonAssociationList && memberInfo.IsManyToManyAlias));
        if (memberInfo.IsAssociationList)
        {
            flag &= memberInfo.IsAggregated || memberInfo.IsManyToMany;
        }

        if (!flag && memberInfo.IsAliased)
        {
            var aliasMemberInfo = FindAliasMemberInfo(memberInfo, findFirstMember: false);
            flag = aliasMemberInfo != null && (IsCollection(aliasMemberInfo) || !memberInfo.IsReadOnly) && IsMemberCloneableCore(aliasMemberInfo);
        }

        if (!flag && !memberInfo.IsPublic)
        {
            flag = memberInfo.IsPersistent;
        }

        return flag;
    }

    private static Type FindMemberInterface(XPMemberInfo memberInfo)
    {
        var interfaces = memberInfo.Owner.ClassType.GetInterfaces();
        foreach (var @interface in interfaces)
        {
            var property = @interface.GetProperty(memberInfo.Name);
            if (property != null && property.PropertyType == memberInfo.MemberType)
            {
                return @interface;
            }
        }
        return null;
    }

    protected virtual bool IsCloneable(XPMemberInfo sourceMemberInfo, XPMemberInfo targetMemberInfo)
    {
        return $"{sourceMemberInfo.Owner.FullName}-{sourceMemberInfo.Name}" == $"{targetMemberInfo.Owner.FullName}-{targetMemberInfo.Name}";
    }

    protected virtual bool HasNonCloneableAttribute(XPMemberInfo memberInfo)
    {
        var typeInfo = _typesInfo.FindTypeInfo(memberInfo.Owner.ClassType);
        if (typeInfo != null)
        {
            return typeInfo.FindMember(memberInfo.Name)?.IsAttributeDefined<NonCloneableAttribute>(recursive: true)
                ?? (memberInfo.FindAttributeInfo(typeof(NonCloneableAttribute)) != null);
        }
        return false;
    }

    public virtual List<XPMemberInfo> GetClonedMembers(List<XPMemberInfo> sourceMembersInfo, List<XPMemberInfo> targetMembersInfo)
    {
        var dictionary = new Dictionary<string, XPMemberInfo>();
        var collectionNames = new List<string>();

        foreach (var sourceMemberInfo in sourceMembersInfo)
        {
            dictionary[sourceMemberInfo.Name] = sourceMemberInfo;
            if (sourceMemberInfo.FindAttributeInfo(typeof(ManyToManyAliasAttribute)) is ManyToManyAliasAttribute manyToManyAliasAttribute)
            {
                collectionNames.Add(manyToManyAliasAttribute.OneToManyCollectionName);
            }
        }

        foreach (string collectionName in collectionNames)
        {
            dictionary.Remove(collectionName);
        }

        var memberInfos = new List<XPMemberInfo>();

        foreach (var targetMemberInfo in targetMembersInfo)
        {
            if (!dictionary.TryGetValue(targetMemberInfo.Name, out XPMemberInfo value) || !IsCloneable(value, targetMemberInfo))
            {
                continue;
            }

            if (IsMemberCloneable(targetMemberInfo) && !memberInfos.Contains(targetMemberInfo))
            {
                memberInfos.Add(targetMemberInfo);
            }
            else if (!IsCollection(targetMemberInfo) && targetMemberInfo.IsReadOnly)
            {
                var xPMemberInfo = FindAliasMemberInfo(targetMemberInfo, findFirstMember: true);
                var xPMemberInfo2 = FindAliasMemberInfo(targetMemberInfo, findFirstMember: false);
                if (xPMemberInfo != null && xPMemberInfo2 != null && IsMemberCloneable(xPMemberInfo) && IsMemberCloneable(xPMemberInfo2) && !memberInfos.Contains(xPMemberInfo))
                {
                    memberInfos.Add(xPMemberInfo);
                }
            }
        }

        return memberInfos;
    }

    public virtual List<XPMemberInfo> GetNotClonedMembers(List<XPMemberInfo> sourceMembersInfo, List<XPMemberInfo> targetMembersInfo)
    {
        var memberInfos = new List<XPMemberInfo>();
        foreach (var sourceMemberInfo in sourceMembersInfo)
        {
            if (!targetMembersInfo.Contains(sourceMemberInfo) && IsMemberCloneable(sourceMemberInfo) && sourceMemberInfo.IsPublic)
            {
                memberInfos.Add(sourceMemberInfo);
            }
        }
        return memberInfos;
    }

    public virtual IXPSimpleObject CreateObject(Session session, Type type)
    {
        return (IXPSimpleObject)Activator.CreateInstance(type, session);
    }

    private static object GetSourceMemberValue(XPMemberInfo targetMemberInfo, IXPSimpleObject simpleObject)
    {
        foreach (var member in simpleObject.ClassInfo.Members)
        {
            if (targetMemberInfo.Name == member.Name && targetMemberInfo.MemberType == member.MemberType)
            {
                return member.GetValue(simpleObject);
            }
        }
        return null;
    }

    public virtual void CopyMemberValue(XPMemberInfo memberInfo, IXPSimpleObject sourceObject, IXPSimpleObject targetObject)
    {
        CheckMemberInObjects(memberInfo, sourceObject, targetObject);
        var sourceMemberValue = GetSourceMemberValue(memberInfo, sourceObject);
        if (sourceMemberValue is not null and IXPSimpleObject)
        {
            ClearTargetObjectProperty(memberInfo, targetObject, memberInfo.IsAggregated);
            var memberValue = CloneReferenceMemberValue(targetObject, memberInfo, (IXPSimpleObject)sourceMemberValue, sourceMemberValue.GetType(), memberInfo.IsAggregated);
            memberInfo.SetValue(targetObject, memberValue);
        }
        else
        {
            memberInfo.SetValue(targetObject, sourceMemberValue);
        }
    }

    private IXPSimpleObject CloneReferenceMemberValue(IXPSimpleObject targetObject, XPMemberInfo memberInfo, IXPSimpleObject sourceObject, Type type, bool createNewObject)
    {
        if (sourceObject == null)
        {
            return null;
        }

        if (_sourceToCloneMap.TryGetValue(sourceObject, out IXPSimpleObject sourceValue))
        {
            return sourceValue;
        }
        else
        {
            var value = InternalCloneTo(sourceObject, type);
            RegisterReferencedObject(targetObject, memberInfo, value);
            return value;
        }
    }

    private void ClearTargetObjectProperty(XPMemberInfo memberInfo, IXPSimpleObject targetObject, bool isAggregated)
    {
        if (IsCollection(memberInfo))
        {
            var list = (IList)memberInfo.GetValue(targetObject);
            var array = new object[list.Count];
            list.CopyTo(array, 0);

            if (isAggregated)
            {
                foreach (object obj in array)
                {
                    if (obj is IXPSimpleObject simpleObject && !_sourceToCloneMap.ContainsValue(simpleObject))
                    {
                        simpleObject.Session.Delete(obj);
                    }
                }
            }

            list.Clear();
        }
        else if (isAggregated)
        {
            var value = memberInfo.GetValue(targetObject);
            if (value is not null and IXPSimpleObject)
            {
                ((IXPSimpleObject)value).Session.Delete(value);
            }
        }
    }

    public virtual void CopyCollection(XPMemberInfo memberInfo, IXPSimpleObject sourceObject, IXPSimpleObject targetObject, bool aggregated)
    {
        if (!IsCollection(memberInfo))
        {
            throw new Exception($"The member '{memberInfo.Name}' is not a collection.");
        }

        var startRecurrentClone = StartRecurrentClone();

        try
        {
            ClearTargetObjectProperty(memberInfo, targetObject, aggregated);

            var list = (IList)GetSourceMemberValue(memberInfo, sourceObject);
            var array = new object[list.Count];
            list.CopyTo(array, 0);

            foreach (var obj in array)
            {
                if (obj is IXPSimpleObject simpleObject && !_sourceToCloneMap.ContainsValue(simpleObject))
                {
                    var value = CloneReferenceMemberValue(targetObject, memberInfo, simpleObject, obj.GetType(), aggregated);
                    ((IList)memberInfo.GetValue(targetObject)).Add(value);
                }
            }

            if (startRecurrentClone)
            {
                UpdateReferencesToClonedObjects();
            }
        }
        finally
        {
            if (startRecurrentClone)
            {
                EndRecurrentClone();
            }
        }
    }

    public object CloneTo(object sourceObject, Type targetType)
    {
        if (sourceObject is not IXPSimpleObject simpleObject)
        {
            throw new InvalidOperationException(nameof(IXPSimpleObject));
        }

        CheckValues(simpleObject, targetType);
        var startRecurrentClone = StartRecurrentClone();

        try
        {
            var value = InternalCloneTo(simpleObject, targetType);
            if (startRecurrentClone)
            {
                UpdateReferencesToClonedObjects();
            }
            return value;
        }
        finally
        {
            if (startRecurrentClone)
            {
                EndRecurrentClone();
            }
        }
    }

    public IXPSimpleObject DefaultCloneTo(IXPSimpleObject sourceObject, Type targetType)
    {
        var startRecurrentClone = StartRecurrentClone();
        IXPSimpleObject targetObject = null;

        try
        {
            CheckValues(sourceObject, targetType);

            targetObject = CreateObject(_targetSession, targetType);
            _sourceToCloneMap[sourceObject] = targetObject;

            var sourceAllPublicMembers = GetAllPublicMembers(sourceObject.ClassInfo);
            var targetAllPublicMembers = GetAllPublicMembers(targetObject.ClassInfo);
            var clonedMembers = GetClonedMembers(sourceAllPublicMembers, targetAllPublicMembers);

            if (targetObject is not null and ISupportInitialize)
            {
                ((ISupportInitialize)targetObject).BeginInit();
            }

            foreach (var clonedMember in clonedMembers)
            {
                if (clonedMember.IsAggregated)
                {
                    CloneMemberValue(sourceObject, targetObject, clonedMember);
                }
            }

            foreach (var clonedMember in clonedMembers)
            {
                if (!clonedMember.IsAggregated)
                {
                    CloneMemberValue(sourceObject, targetObject, clonedMember);
                }
            }

            var notClonedInfoMemberName = GetNotClonedInfoMemberName(targetObject.ClassInfo);
            if (notClonedInfoMemberName != null)
            {
                var value = FormatNotClonedMemberValues((string)notClonedInfoMemberName.GetValue(targetObject), sourceObject, GetNotClonedMembers(sourceAllPublicMembers, targetAllPublicMembers));
                notClonedInfoMemberName.SetValue(targetObject, value);
            }

            if (startRecurrentClone)
            {
                UpdateReferencesToClonedObjects();
            }

            return targetObject;
        }
        finally
        {
            if (startRecurrentClone)
            {
                EndRecurrentClone();
            }

            if (targetObject is not null and ISupportInitialize)
            {
                ((ISupportInitialize)targetObject).EndInit();
            }
        }
    }

    private void CloneMemberValue(IXPSimpleObject sourceObject, IXPSimpleObject targetObject, XPMemberInfo memberInfo)
    {
        if (IsCollection(memberInfo))
        {
            CheckMemberInObjects(memberInfo, sourceObject, targetObject);
            if (GetSourceMemberValue(memberInfo, sourceObject) != null)
            {
                CopyCollection(memberInfo, sourceObject, targetObject, memberInfo.IsAggregated);
            }
        }
        else
        {
            CopyMemberValue(memberInfo, sourceObject, targetObject);
        }
    }

    private static bool IsCollection(XPMemberInfo memberInfo)
    {
        if (!memberInfo.IsAssociationList)
        {
            return memberInfo.IsNonAssociationList;
        }
        return true;
    }
}
